﻿// The MIT License(MIT)
// =====================
//
// Copyright © `2015-2017` `Lucas Meijer`
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the “Software”), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace Mono.Linker.Tests.Extensions
{
	public class NPath : IEquatable<NPath>, IComparable
	{
		private static readonly StringComparison PathStringComparison = IsLinux() ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;

		private readonly string[] _elements;
		private readonly bool _isRelative;
		private readonly string _driveLetter;

		#region construction

		public NPath(string path)
		{
			if (path == null)
				throw new ArgumentNullException();

			path = ParseDriveLetter(path, out _driveLetter);

			if (path == "/")
			{
				_isRelative = false;
				_elements = new string[] { };
			}
			else
			{
				var split = path.Split('/', '\\');

				_isRelative = _driveLetter == null && IsRelativeFromSplitString(split);

				_elements = ParseSplitStringIntoElements(split.Where(s => s.Length > 0).ToArray());
			}
		}

		private NPath(string[] elements, bool isRelative, string driveLetter)
		{
			_elements = elements;
			_isRelative = isRelative;
			_driveLetter = driveLetter;
		}

		private string[] ParseSplitStringIntoElements(IEnumerable<string> inputs)
		{
			var stack = new List<string>();

			foreach (var input in inputs.Where(input => input.Length != 0))
			{
				if (input == ".")
				{
					if ((stack.Count > 0) && (stack.Last() != "."))
						continue;
				}
				else if (input == "..")
				{
					if (HasNonDotDotLastElement(stack))
					{
						stack.RemoveAt(stack.Count - 1);
						continue;
					}
					if (!_isRelative)
						throw new ArgumentException("You cannot create a path that tries to .. past the root");
				}
				stack.Add(input);
			}
			return stack.ToArray();
		}

		private static bool HasNonDotDotLastElement(List<string> stack)
		{
			return stack.Count > 0 && stack[stack.Count - 1] != "..";
		}

		private string ParseDriveLetter(string path, out string driveLetter)
		{
			if (path.Length >= 2 && path[1] == ':')
			{
				driveLetter = path[0].ToString();
				return path.Substring(2);
			}

			driveLetter = null;
			return path;
		}

		private static bool IsRelativeFromSplitString(string[] split)
		{
			if (split.Length < 2)
				return true;

			return split[0].Length != 0 || !split.Any(s => s.Length > 0);
		}

		public NPath Combine(params string[] append)
		{
			return Combine(append.Select(a => new NPath(a)).ToArray());
		}

		public NPath Combine(params NPath[] append)
		{
			if (!append.All(p => p.IsRelative))
				throw new ArgumentException("You cannot .Combine a non-relative path");

			return new NPath(ParseSplitStringIntoElements(_elements.Concat(append.SelectMany(p => p._elements))), _isRelative, _driveLetter);
		}

		public NPath Parent
		{
			get
			{
				if (_elements.Length == 0)
					throw new InvalidOperationException("Parent is called on an empty path");

				var newElements = _elements.Take(_elements.Length - 1).ToArray();

				return new NPath(newElements, _isRelative, _driveLetter);
			}
		}

		public NPath RelativeTo(NPath path)
		{
			if (!IsChildOf(path))
			{
				if (!IsRelative && !path.IsRelative && _driveLetter != path._driveLetter)
					throw new ArgumentException("Path.RelativeTo() was invoked with two paths that are on different volumes. invoked on: " + ToString() + " asked to be made relative to: " + path);

				NPath commonParent = null;
				foreach (var parent in RecursiveParents)
				{
					commonParent = path.RecursiveParents.FirstOrDefault(otherParent => otherParent == parent);

					if (commonParent != null)
						break;
				}

				if (commonParent == null)
					throw new ArgumentException("Path.RelativeTo() was unable to find a common parent between " + ToString() + " and " + path);

				if (IsRelative && path.IsRelative && commonParent.IsEmpty())
					throw new ArgumentException("Path.RelativeTo() was invoked with two relative paths that do not share a common parent.  Invoked on: " + ToString() + " asked to be made relative to: " + path);

				var depthDiff = path.Depth - commonParent.Depth;
				return new NPath(Enumerable.Repeat("..", depthDiff).Concat(_elements.Skip(commonParent.Depth)).ToArray(), true, null);
			}

			return new NPath(_elements.Skip(path._elements.Length).ToArray(), true, null);
		}

		public NPath ChangeExtension(string extension)
		{
			ThrowIfRoot();

			var newElements = (string[])_elements.Clone();
			newElements[newElements.Length - 1] = Path.ChangeExtension(_elements[_elements.Length - 1], WithDot(extension));
			if (extension == string.Empty)
				newElements[newElements.Length - 1] = newElements[newElements.Length - 1].TrimEnd('.');
			return new NPath(newElements, _isRelative, _driveLetter);
		}
		#endregion construction

		#region inspection

		public bool IsRelative
		{
			get { return _isRelative; }
		}

		public string FileName
		{
			get
			{
				ThrowIfRoot();

				return _elements.Last();
			}
		}

		public string FileNameWithoutExtension
		{
			get { return Path.GetFileNameWithoutExtension(FileName); }
		}

		public IEnumerable<string> Elements
		{
			get { return _elements; }
		}

		public int Depth
		{
			get { return _elements.Length; }
		}

		public bool Exists(string append = "")
		{
			return Exists(new NPath(append));
		}

		public bool Exists(NPath append)
		{
			return FileExists(append) || DirectoryExists(append);
		}

		public bool DirectoryExists(string append = "")
		{
			return DirectoryExists(new NPath(append));
		}

		public bool DirectoryExists(NPath append)
		{
			return Directory.Exists(Combine(append).ToString());
		}

		public bool FileExists(string append = "")
		{
			return FileExists(new NPath(append));
		}

		public bool FileExists(NPath append)
		{
			return File.Exists(Combine(append).ToString());
		}

		public string ExtensionWithDot
		{
			get
			{
				if (IsRoot)
					throw new ArgumentException("A root directory does not have an extension");

				var last = _elements.Last();
				var index = last.LastIndexOf(".");
				if (index < 0) return String.Empty;
				return last.Substring(index);
			}
		}

		public string InQuotes()
		{
			return "\"" + ToString() + "\"";
		}

		public string InQuotes(SlashMode slashMode)
		{
			return "\"" + ToString(slashMode) + "\"";
		}

		public override string ToString()
		{
			return ToString(SlashMode.Native);
		}

		public string ToString(SlashMode slashMode)
		{
			// Check if it's linux root /
			if (IsRoot && string.IsNullOrEmpty(_driveLetter))
				return Slash(slashMode).ToString();

			if (_isRelative && _elements.Length == 0)
				return ".";

			var sb = new StringBuilder();
			if (_driveLetter != null)
			{
				sb.Append(_driveLetter);
				sb.Append(":");
			}
			if (!_isRelative)
				sb.Append(Slash(slashMode));
			var first = true;
			foreach (var element in _elements)
			{
				if (!first)
					sb.Append(Slash(slashMode));

				sb.Append(element);
				first = false;
			}
			return sb.ToString();
		}

		public static implicit operator string(NPath path)
		{
			return path.ToString();
		}

		static char Slash(SlashMode slashMode)
		{
			switch (slashMode)
			{
				case SlashMode.Backward:
					return '\\';
				case SlashMode.Forward:
					return '/';
				default:
					return Path.DirectorySeparatorChar;
			}
		}

		public override bool Equals(Object obj)
		{
			if (obj == null)
				return false;

			// If parameter cannot be cast to Point return false.
			var p = obj as NPath;
			if ((Object)p == null)
				return false;

			return Equals(p);
		}

		public bool Equals(NPath p)
		{
			if (p._isRelative != _isRelative)
				return false;

			if (!string.Equals(p._driveLetter, _driveLetter, PathStringComparison))
				return false;

			if (p._elements.Length != _elements.Length)
				return false;

			for (var i = 0; i != _elements.Length; i++)
				if (!string.Equals(p._elements[i], _elements[i], PathStringComparison))
					return false;

			return true;
		}

		public static bool operator ==(NPath a, NPath b)
		{
			// If both are null, or both are same instance, return true.
			if (ReferenceEquals(a, b))
				return true;

			// If one is null, but not both, return false.
			if (((object)a == null) || ((object)b == null))
				return false;

			// Return true if the fields match:
			return a.Equals(b);
		}

		public override int GetHashCode()
		{
			unchecked
			{
				int hash = 17;
				// Suitable nullity checks etc, of course :)
				hash = hash * 23 + _isRelative.GetHashCode();
				foreach (var element in _elements)
					hash = hash * 23 + element.GetHashCode();
				if (_driveLetter != null)
					hash = hash * 23 + _driveLetter.GetHashCode();
				return hash;
			}
		}

		public int CompareTo(object obj)
		{
			if (obj == null)
				return -1;

			return this.ToString().CompareTo(((NPath)obj).ToString());
		}

		public static bool operator !=(NPath a, NPath b)
		{
			return !(a == b);
		}

		public bool HasExtension(params string[] extensions)
		{
			var extensionWithDotLower = ExtensionWithDot.ToLower();
			return extensions.Any(e => WithDot(e).ToLower() == extensionWithDotLower);
		}

		private static string WithDot(string extension)
		{
			return extension.StartsWith(".") ? extension : "." + extension;
		}

		private bool IsEmpty()
		{
			return _elements.Length == 0;
		}

		public bool IsRoot
		{
			get { return _elements.Length == 0 && !_isRelative; }
		}

		#endregion inspection

		#region directory enumeration

		public IEnumerable<NPath> Files(string filter, bool recurse = false)
		{
			return Directory.GetFiles(ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s));
		}

		public IEnumerable<NPath> Files(bool recurse = false)
		{
			return Files("*", recurse);
		}

		public IEnumerable<NPath> Contents(string filter, bool recurse = false)
		{
			return Files(filter, recurse).Concat(Directories(filter, recurse));
		}

		public IEnumerable<NPath> Contents(bool recurse = false)
		{
			return Contents("*", recurse);
		}

		public IEnumerable<NPath> Directories(string filter, bool recurse = false)
		{
			return Directory.GetDirectories(ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s));
		}

		public IEnumerable<NPath> Directories(bool recurse = false)
		{
			return Directories("*", recurse);
		}

		#endregion

		#region filesystem writing operations
		public NPath CreateFile()
		{
			ThrowIfRelative();
			ThrowIfRoot();
			EnsureParentDirectoryExists();
			File.WriteAllBytes(ToString(), new byte[0]);
			return this;
		}

		public NPath CreateFile(string file)
		{
			return CreateFile(new NPath(file));
		}

		public NPath CreateFile(NPath file)
		{
			if (!file.IsRelative)
				throw new ArgumentException("You cannot call CreateFile() on an existing path with a non relative argument");
			return Combine(file).CreateFile();
		}

		public NPath CreateDirectory()
		{
			ThrowIfRelative();

			if (IsRoot)
				throw new NotSupportedException("CreateDirectory is not supported on a root level directory because it would be dangerous:" + ToString());

			Directory.CreateDirectory(ToString());
			return this;
		}

		public NPath CreateDirectory(string directory)
		{
			return CreateDirectory(new NPath(directory));
		}

		public NPath CreateDirectory(NPath directory)
		{
			if (!directory.IsRelative)
				throw new ArgumentException("Cannot call CreateDirectory with an absolute argument");

			return Combine(directory).CreateDirectory();
		}

		public NPath Copy(string dest)
		{
			return Copy(new NPath(dest));
		}

		public NPath Copy(string dest, Func<NPath, bool> fileFilter)
		{
			return Copy(new NPath(dest), fileFilter);
		}

		public NPath Copy(NPath dest)
		{
			return Copy(dest, p => true);
		}

		public NPath Copy(NPath dest, Func<NPath, bool> fileFilter)
		{
			ThrowIfRelative();
			if (dest.IsRelative)
				dest = Parent.Combine(dest);

			if (dest.DirectoryExists())
				return CopyWithDeterminedDestination(dest.Combine(FileName), fileFilter);

			return CopyWithDeterminedDestination(dest, fileFilter);
		}

		public NPath MakeAbsolute()
		{
			if (!IsRelative)
				return this;

			return NPath.CurrentDirectory.Combine(this);
		}

		NPath CopyWithDeterminedDestination(NPath absoluteDestination, Func<NPath, bool> fileFilter)
		{
			if (absoluteDestination.IsRelative)
				throw new ArgumentException("absoluteDestination must be absolute");

			if (FileExists())
			{
				if (!fileFilter(absoluteDestination))
					return null;

				absoluteDestination.EnsureParentDirectoryExists();

				File.Copy(ToString(), absoluteDestination.ToString(), true);
				return absoluteDestination;
			}

			if (DirectoryExists())
			{
				absoluteDestination.EnsureDirectoryExists();
				foreach (var thing in Contents())
					thing.CopyWithDeterminedDestination(absoluteDestination.Combine(thing.RelativeTo(this)), fileFilter);
				return absoluteDestination;
			}

			throw new ArgumentException("Copy() called on path that doesnt exist: " + ToString());
		}

		public void Delete(DeleteMode deleteMode = DeleteMode.Normal)
		{
			ThrowIfRelative();

			if (IsRoot)
				throw new NotSupportedException("Delete is not supported on a root level directory because it would be dangerous:" + ToString());

			if (FileExists())
				File.Delete(ToString());
			else if (DirectoryExists())
				try
				{
					Directory.Delete(ToString(), true);
				}
				catch (IOException)
				{
					if (deleteMode == DeleteMode.Normal)
						throw;
				}
			else
				throw new InvalidOperationException("Trying to delete a path that does not exist: " + ToString());
		}

		public void DeleteIfExists(DeleteMode deleteMode = DeleteMode.Normal)
		{
			ThrowIfRelative();

			if (FileExists() || DirectoryExists())
				Delete(deleteMode);
		}

		public NPath DeleteContents()
		{
			ThrowIfRelative();

			if (IsRoot)
				throw new NotSupportedException("DeleteContents is not supported on a root level directory because it would be dangerous:" + ToString());

			if (FileExists())
				throw new InvalidOperationException("It is not valid to perform this operation on a file");

			if (DirectoryExists())
			{
				try
				{
					Files().Delete();
					Directories().Delete();
				}
				catch (IOException)
				{
					if (Files(true).Any())
						throw;
				}

				return this;
			}

			return EnsureDirectoryExists();
		}

		public static NPath CreateTempDirectory(string myprefix)
		{
			var random = new Random();
			while (true)
			{
				var candidate = new NPath(Path.GetTempPath() + "/" + myprefix + "_" + random.Next());
				if (!candidate.Exists())
					return candidate.CreateDirectory();
			}
		}

		public NPath Move(string dest)
		{
			return Move(new NPath(dest));
		}

		public NPath Move(NPath dest)
		{
			ThrowIfRelative();

			if (IsRoot)
				throw new NotSupportedException("Move is not supported on a root level directory because it would be dangerous:" + ToString());

			if (dest.IsRelative)
				return Move(Parent.Combine(dest));

			if (dest.DirectoryExists())
				return Move(dest.Combine(FileName));

			if (FileExists())
			{
				dest.EnsureParentDirectoryExists();
				File.Move(ToString(), dest.ToString());
				return dest;
			}

			if (DirectoryExists())
			{
				Directory.Move(ToString(), dest.ToString());
				return dest;
			}

			throw new ArgumentException("Move() called on a path that doesn't exist: " + ToString());
		}

		#endregion

		#region special paths

		public static NPath CurrentDirectory
		{
			get
			{
				return new NPath(Directory.GetCurrentDirectory());
			}
		}

		public static NPath HomeDirectory
		{
			get
			{
				if (Path.DirectorySeparatorChar == '\\')
					return new NPath(Environment.GetEnvironmentVariable("USERPROFILE"));
				return new NPath(Environment.GetEnvironmentVariable("HOME"));
			}
		}

		public static NPath SystemTemp
		{
			get
			{
				return new NPath(Path.GetTempPath());
			}
		}

		#endregion

		private void ThrowIfRelative()
		{
			if (_isRelative)
				throw new ArgumentException("You are attempting an operation on a Path that requires an absolute path, but the path is relative");
		}

		private void ThrowIfRoot()
		{
			if (IsRoot)
				throw new ArgumentException("You are attempting an operation that is not valid on a root level directory");
		}

		public NPath EnsureDirectoryExists(string append = "")
		{
			return EnsureDirectoryExists(new NPath(append));
		}

		public NPath EnsureDirectoryExists(NPath append)
		{
			var combined = Combine(append);
			if (combined.DirectoryExists())
				return combined;
			combined.EnsureParentDirectoryExists();
			combined.CreateDirectory();
			return combined;
		}

		public NPath EnsureParentDirectoryExists()
		{
			var parent = Parent;
			parent.EnsureDirectoryExists();
			return parent;
		}

		public NPath FileMustExist()
		{
			if (!FileExists())
				throw new FileNotFoundException("File was expected to exist : " + ToString());

			return this;
		}

		public NPath DirectoryMustExist()
		{
			if (!DirectoryExists())
				throw new DirectoryNotFoundException("Expected directory to exist : " + ToString());

			return this;
		}

		public bool IsChildOf(string potentialBasePath)
		{
			return IsChildOf(new NPath(potentialBasePath));
		}

		public bool IsChildOf(NPath potentialBasePath)
		{
			if ((IsRelative && !potentialBasePath.IsRelative) || !IsRelative && potentialBasePath.IsRelative)
				throw new ArgumentException("You can only call IsChildOf with two relative paths, or with two absolute paths");

			// If the other path is the root directory, then anything is a child of it as long as it's not a Windows path
			if (potentialBasePath.IsRoot)
			{
				if (_driveLetter != potentialBasePath._driveLetter)
					return false;
				return true;
			}

			if (IsEmpty())
				return false;

			if (Equals(potentialBasePath))
				return true;

			return Parent.IsChildOf(potentialBasePath);
		}

		public IEnumerable<NPath> RecursiveParents
		{
			get
			{
				var candidate = this;
				while (true)
				{
					if (candidate.IsEmpty())
						yield break;

					candidate = candidate.Parent;
					yield return candidate;
				}
			}
		}

		public NPath ParentContaining(string needle)
		{
			return ParentContaining(new NPath(needle));
		}

		public NPath ParentContaining(NPath needle)
		{
			ThrowIfRelative();

			return RecursiveParents.FirstOrDefault(p => p.Exists(needle));
		}

		public NPath WriteAllText(string contents)
		{
			ThrowIfRelative();
			EnsureParentDirectoryExists();
			File.WriteAllText(ToString(), contents);
			return this;
		}

		public string ReadAllText()
		{
			ThrowIfRelative();
			return File.ReadAllText(ToString());
		}

		public NPath WriteAllLines(string[] contents)
		{
			ThrowIfRelative();
			EnsureParentDirectoryExists();
			File.WriteAllLines(ToString(), contents);
			return this;
		}

		public string[] ReadAllLines()
		{
			ThrowIfRelative();
			return File.ReadAllLines(ToString());
		}

		public IEnumerable<NPath> CopyFiles(NPath destination, bool recurse, Func<NPath, bool> fileFilter = null)
		{
			destination.EnsureDirectoryExists();
			return Files(recurse).Where(fileFilter ?? AlwaysTrue).Select(file => file.Copy(destination.Combine(file.RelativeTo(this)))).ToArray();
		}

		public IEnumerable<NPath> MoveFiles(NPath destination, bool recurse, Func<NPath, bool> fileFilter = null)
		{
			if (IsRoot)
				throw new NotSupportedException("MoveFiles is not supported on this directory because it would be dangerous:" + ToString());

			destination.EnsureDirectoryExists();
			return Files(recurse).Where(fileFilter ?? AlwaysTrue).Select(file => file.Move(destination.Combine(file.RelativeTo(this)))).ToArray();
		}

		static bool AlwaysTrue(NPath p)
		{
			return true;
		}

		private static bool IsLinux()
		{
			return Directory.Exists("/proc");
		}
	}

	public static class Extensions
	{
		public static IEnumerable<NPath> Copy(this IEnumerable<NPath> self, string dest)
		{
			return Copy(self, new NPath(dest));
		}

		public static IEnumerable<NPath> Copy(this IEnumerable<NPath> self, NPath dest)
		{
			if (dest.IsRelative)
				throw new ArgumentException("When copying multiple files, the destination cannot be a relative path");
			dest.EnsureDirectoryExists();
			return self.Select(p => p.Copy(dest.Combine(p.FileName))).ToArray();
		}

		public static IEnumerable<NPath> Move(this IEnumerable<NPath> self, string dest)
		{
			return Move(self, new NPath(dest));
		}

		public static IEnumerable<NPath> Move(this IEnumerable<NPath> self, NPath dest)
		{
			if (dest.IsRelative)
				throw new ArgumentException("When moving multiple files, the destination cannot be a relative path");
			dest.EnsureDirectoryExists();
			return self.Select(p => p.Move(dest.Combine(p.FileName))).ToArray();
		}

		public static IEnumerable<NPath> Delete(this IEnumerable<NPath> self)
		{
			foreach (var p in self)
				p.Delete();
			return self;
		}

		public static IEnumerable<string> InQuotes(this IEnumerable<NPath> self, SlashMode forward = SlashMode.Native)
		{
			return self.Select(p => p.InQuotes(forward));
		}

		public static NPath ToNPath(this string path)
		{
			return new NPath(path);
		}
	}

	public enum SlashMode
	{
		Native,
		Forward,
		Backward
	}

	public enum DeleteMode
	{
		Normal,
		Soft
	}
}